跳到主要内容

React 官网教程跟学

· 阅读需 6 分钟

我对于前端开发的感受是,实际工程和教程实例代码的差距很大,还有很容易陷在改局部需求,最终变成查文档和写局部 JS 代码的工具人,而失去对项目的总体认识。因为前端一般项目庞大,引入库众多,加上每个库都有自己独特的写法,写多了,就只知其一,不知其二了。

综上,为了建立对 react 项目的整体认知,我就第 N 次过一遍官网的教程,同时记录那些概念和项目结构。

为了方便,我用到的开发环境为 codesanbox 提供的在线 react 环境。即时渲染,非常方便。

项目结构

react项目结构

public有一个index.html, 是 react 项目的唯一网页入口,也就是常说的单页面应用(SPA)。里面有一个 root 节点。src/index.js就是网页的根脚本,获取 root dom 作为组件树挂载的地方。src/App.js是一个范例组件,通过export default导出,index.js导入并渲染。

组件

react 就是由组件组成的。组件是一个返回标签的 JS 函数。区分 react 组件和 html 标签的方法是,react 组件以大写字母开头,遵守 camel 命名。

组件内定义的标签是用 JSX 语法写成的,有别于 html 语法。JSX 最大的特点就是在标签里有 js 逻辑。JSX 有很多语法规则,我这里记录一下我认为比较重要的部分:

  1. return 之后一般会跟一个括号,把 JSX 标签包起来。
  2. 用大括号{}来插入 JavaScript 表达式,而且还接受函数、对象。

渲染列表

渲染列表两个要素:map 函数和 li 标签。

const listItems = products.map((product) => (
<li key={product.id}>{product.title}</li>
));

return <ul>{listItems}</ul>;

注意,li 标签一定要有一个key属性,用于 vdom 渲染。

useState / Prop

useState 是一个钩子函数,管理变量用的。Prop 是指传递给组件的参数。

export default function MyApp() {
const [count, setCount] = useState(0);

function handleClick() {
setCount(count + 1);
}

return (
<div>
<h1>Counters that update together</h1>
<MyButton count={count} onClick={handleClick} />
<MyButton count={count} onClick={handleClick} />
</div>
);
}

function MyButton({ count, onClick }) {
return <button onClick={onClick}>Clicked {count} times</button>;
}

countonClick就是 props,它们被MyButton组件用一个花括号包起来一起接收。

其实,传递函数类型的 prop 时,应该用匿名函数的形式:

<MyButton
count={count}
onClick={() => {
handleClick();
}}
/>

这样,如果 handleClick 需要传入参数,也可以正常执行。

触发时机

import { useState } from "react";

function Square({ value, onSquareClick }) {
return (
<button className="square" onClick={onSquareClick}>
{value}
</button>
);
}

export default function Board() {
const [xIsNext, setXIsNext] = useState(true);
const [squares, setSquares] = useState(Array(9).fill(null));

function handleClick(i) {
if (calculateWinner(squares) || squares[i]) {
return;
}
const nextSquares = squares.slice();
if (xIsNext) {
nextSquares[i] = "X";
} else {
nextSquares[i] = "O";
}
setSquares(nextSquares);
setXIsNext(!xIsNext);
}

const winner = calculateWinner(squares);
let status;
if (winner) {
status = "Winner: " + winner;
} else {
status = "Next player: " + (xIsNext ? "X" : "O");
}

return (
<>
<div className="status">{status}</div>
<div className="board-row">
<Square value={squares[0]} onSquareClick={() => handleClick(0)} />
<Square value={squares[1]} onSquareClick={() => handleClick(1)} />
<Square value={squares[2]} onSquareClick={() => handleClick(2)} />
</div>
<div className="board-row">
<Square value={squares[3]} onSquareClick={() => handleClick(3)} />
<Square value={squares[4]} onSquareClick={() => handleClick(4)} />
<Square value={squares[5]} onSquareClick={() => handleClick(5)} />
</div>
<div className="board-row">
<Square value={squares[6]} onSquareClick={() => handleClick(6)} />
<Square value={squares[7]} onSquareClick={() => handleClick(7)} />
<Square value={squares[8]} onSquareClick={() => handleClick(8)} />
</div>
</>
);
}

function calculateWinner(squares) {
const lines = [
[0, 1, 2],
[3, 4, 5],
[6, 7, 8],
[0, 3, 6],
[1, 4, 7],
[2, 5, 8],
[0, 4, 8],
[2, 4, 6],
];
for (let i = 0; i < lines.length; i++) {
const [a, b, c] = lines[i];
if (squares[a] && squares[a] === squares[b] && squares[a] === squares[c]) {
return squares[a];
}
}
return null;
}

这是官网井字棋游戏的部分代码,我读到这里产生一个疑问,为什么calculateWinner会在 status 那里将最终赢家给出来。我想的是 winner 这一段只会在初始化执行一次。后面我明白,每当玩家点击方格并触发 handleClick(i) 函数时,squares 的值会更新,然后 setSquares(nextSquares) 会触发组件的重新渲染,接着 calculateWinner(squares) 会再次被调用,检查当前棋盘是否存在获胜者。由于 React 中状态更新后会触发组件重新渲染,calculateWinner 每次都会在组件渲染时被调用以检查当前的胜利情况。

状态提升

因为 reactjs 是基于单向数据流的,组件的状态通过 props 向下传递,从父组件传递到子组件。子组件不能直接修改从父组件传来的数据,而是通过触发父组件的回调函数来更新父组件的状态,父组件状态变化后再将新的状态传给子组件。所以,状态需要被提升到父组件中进行管理。